package cn.edu.dgut.css.sai.demo.wechat.miniprogramservicea;

import cn.edu.dgut.css.sai.demo.wechat.miniprogramservicea.utils.KeyStoreKeyFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.*;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.Charset;
import java.security.KeyPair;
import java.security.interfaces.RSAPublicKey;
import java.util.Map;
import java.util.Optional;

/**
 * @author sai
 * @since 2021/1/11
 */
@SpringBootApplication
public class MiniProgramServiceAApplication {

    public static void main(String[] args) {
        SpringApplication.run(MiniProgramServiceAApplication.class, args);
    }

    @RestController
    static class HelloWorldController {

        final RestTemplate restTemplate;
        final WeChatProperties weChatProperties;

        HelloWorldController(RestTemplateBuilder restTemplateBuilder, WeChatProperties weChatProperties) {
            this.restTemplate = restTemplateBuilder.build();
            this.weChatProperties = weChatProperties;
        }

        // 测试用Api
        @GetMapping("/service-a")
        String index(Authentication authentication) {
            // authentication 的类型为 JwtAuthenticationToken
            System.out.println("authentication = " + authentication);
            // authentication.getName() 为 Jwt 中 sub 的字段值
            System.out.println("authentication.getName() = " + authentication.getName());

            String apiAccessToken = getApiAccessToken(weChatProperties.getMiniProgram().getAppId(), weChatProperties.getMiniProgram().getAppSecret());
            System.out.println("apiAccessToken = " + apiAccessToken);

            String secCheck = msgSecCheck(apiAccessToken, "黎志雄，网络空间安全学院");
            System.out.println("secCheck = " + secCheck);

            return "黎志雄，企业级开发框架实践";
        }

        // 测试cors配置
        @GetMapping("/cors")
        String corsTest() {
            return "Cors配置成功";
        }

        // auth.getAccessToken
        // 获取小程序全局唯一后台接口调用凭据（access_token）。调用绝大多数后台接口时都需使用 access_token，开发者需要进行妥善保存。
        // 凭证有效时间，单位：秒。目前是7200秒之内的值。
        // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
        private String getApiAccessToken(Object... uriVariables) {
            // @formatter:off
            UriComponents uriComponents = UriComponentsBuilder
                                                .fromUriString(weChatProperties.getMiniProgram().getAccessTokenApi())
                                                .buildAndExpand(uriVariables)
                                                .encode();
            // 构造请求正文
            RequestEntity<Void> accessTokenRequestEntity = RequestEntity.get(uriComponents.toUri()).build();
            // 请求access_token
            ResponseEntity<Map<String, Object>> apiAccessTokenResponseEntity = restTemplate.exchange(accessTokenRequestEntity, new ParameterizedTypeReference<>() {});
            // @formatter:on

            return Optional.ofNullable(apiAccessTokenResponseEntity.getBody()).map(bodyMap -> bodyMap.get("access_token").toString()).orElseThrow(() -> new RuntimeException("获取access_token失败"));
        }

        // security.msgSecCheck
        // 检查一段文本是否含有违法违规内容。
        // 应用场景举例：
        //      用户个人资料违规文字检测；
        //      媒体新闻类用户发表文章，评论内容检测；
        //      游戏类用户编辑上传的素材(如答题类小游戏用户上传的问题及答案)检测等。 频率限制：单个 appId 调用上限为 4000 次/分钟，2,000,000 次/天*
        // https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.msgSecCheck.html
        @SuppressWarnings("SameParameterValue")
        private String msgSecCheck(String apiAccessToken, String content) {
            // @formatter:off
            UriComponents uriComponents = UriComponentsBuilder
                                                .fromUriString("https://api.weixin.qq.com/wxa/msg_sec_check?access_token={ACCESS_TOKEN}")
                                                .buildAndExpand(apiAccessToken)
                                                .encode();
            // 构造请求正文
            RequestEntity<Map<String, String>> requestEntity = RequestEntity.post(uriComponents.toUri()).body(Map.of("content", content));
            ResponseEntity<Map<String, Object>> responseEntity = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<>() {});
            // @formatter:on

            return Optional.ofNullable(responseEntity.getBody()).map(bodyMap -> bodyMap.get("errcode").toString()).orElseThrow();
        }

    }

}

@Configuration
@EnableConfigurationProperties(WeChatProperties.class)
class SecurityConfig {

    /**
     * 定义service-a微服务的安全过滤链
     *
     * @see BearerTokenResolver
     * @see DefaultBearerTokenResolver
     * @see BearerTokenAuthenticationFilter
     * @see OAuth2ResourceServerJwtConfiguration
     * @see JwtDecoder
     * @see NimbusJwtDecoder
     * @see BearerTokenAuthenticationToken 验证Jwt前，会先封装为这个类型的 {@link Authentication}。
     * @see JwtAuthenticationProvider
     * @see JwtAuthenticationConverter 把 Jwt 的字符串转换为 {@link JwtAuthenticationToken}，它是{@link Authentication}的子类。
     * {@link JwtAuthenticationToken}会默认把 Jwt 中 sub 字段作为 name 的值，可以在{@link JwtAuthenticationConverter}中修改这个默认字段；
     * {@link JwtAuthenticationToken}会默认把 Jwt 设置为  getPrincipal()方法的返回值。
     */
    @SuppressWarnings("JavadocReference")
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        // 配置跨域策略，会引用 CorsConfigurationSource 的Bean的配置，需要自己定义。
        http.cors();

        // 设置所有http请求都必需认证用户才能访问，必须设置。
        // 可以根据系统需求定制URL的访问权限。
        http.authorizeRequests().antMatchers("/cors").permitAll()
                                .anyRequest().authenticated();

        // 默认开启了 http.csrf()，但 jwt() 后，会忽略header含有Bearer token的请求。
        http.oauth2ResourceServer().jwt();

        // 设置不创建 HttpSession
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // 设置请求api时没有token，或权限不足时的处理响应的逻辑。
        // BearerTokenAuthenticationFilter 当发现请求没有 Authorization: Bearer 的头部时，就会调用 AuthenticationEntryPoint 处理响应。
        // 详细看 BearerTokenAuthenticationFilter的doFilterInternal方法源码。
        http.exceptionHandling()
                .authenticationEntryPoint(SecurityConfig::authenticationEntryPoint)
                .accessDeniedHandler(SecurityConfig::accessDeniedHandle);

        return http.build();
    }

    // 当用户尝试访问安全的REST资源而不提供任何凭据时，将调用此方法发送401响应
    private static void authenticationEntryPoint(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        editResponseHeader(response);

        Map<String, Object> err = Map.of("errcode", 1111, "errmsg", "请登录!");
        response.getWriter().write(new ObjectMapper().writeValueAsString(err));
    }

    /**
     * @see AccessDeniedHandlerImpl#handle(HttpServletRequest, HttpServletResponse, AccessDeniedException)
     */
    private static void accessDeniedHandle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);
        response.setStatus(HttpStatus.FORBIDDEN.value());
        editResponseHeader(response);

        Map<String, Object> err = Map.of("errcode", 2222, "errmsg", "权限不足，请联系管理员!");
        response.getWriter().write(new ObjectMapper().writeValueAsString(err));
    }

    private static void editResponseHeader(HttpServletResponse response) {
        response.setCharacterEncoding(Charset.defaultCharset().displayName());// 解决中文乱码
        response.addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE);
    }

    /**
     * 定义JwtDecoder
     *
     * <p>如果oauth2ResourceServer使用jwt进行认证时，必须定义一个 JwtDecoder 的 Bean，否则程序启动时会报异常。</p>
     * <p>这里实例化一个{@link NimbusJwtDecoder}，并指定从JKS密钥库文件里读取公钥。</p>
     */
    @Bean
    JwtDecoder jwtDecoderByPublicKeyValue(KeyPair keyPair) {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        // @formatter:off
        return NimbusJwtDecoder
                    .withPublicKey(publicKey)
                    .signatureAlgorithm(SignatureAlgorithm.RS256)
                    .build();
        // @formatter:on
    }

    /**
     * 加载 JKS密钥库文件，此密钥库包含 RS256 的密钥对。
     *
     * <p>一定要注意：JKS密钥库文件 必须与 uaa项目的一致。</p>
     *
     * <p>在resource目录下生成jwt使用的jks密钥库文件:</p>
     * <p>
     * keytool -genkeypair -alias wechatjks -keyalg RSA -keypass lizhx@dgut.edu.cn -keystore wechat.jks -storepass lizhx@dgut.edu.cn -validity 36500
     * </p>
     * <p>后面的询问，最后一步输入 y ,其它回车即可。</p>
     * <p>
     * 上面的命令生成 2,048 位 RSA 密钥对和自签名证书 (SHA256withRSA) (有效期为 36,500 天)
     * </p>
     *
     * @return {@link KeyPair}
     */
    @Bean
    KeyPair keyPair() {
        // @formatter:off
        return new KeyStoreKeyFactory(new ClassPathResource("wechat.jks"), "lizhx@dgut.edu.cn".toCharArray())// 密钥对的文件、密码
                        .getKeyPair("wechatjks");// 生成密钥对时设置的别名-alias
        // @formatter:on
    }

    /**
     * 配置CORS跨域请求
     *
     * <li>参考 ：https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#cors </li>
     * <li>https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS</li>
     * <li>如果配置成功，则在http响应中会添加两个header:
     *      Access-Control-Allow-Origin: *
     *      Access-Control-Allow-Methods: GET,HEAD,POST,PUT,DELETE
     * </li>
     * <li>使用curl测试跨域： curl -v -H "Access-Control-Request-Method: GET" -H "Origin: http://localhost:9000" -X OPTIONS http://localhost:8081/cors </li>
     */
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.applyPermitDefaultValues();
        // 允许所有http请求方法
        // configuration.setAllowedMethods(Collections.singletonList(CorsConfiguration.ALL));
        configuration.addAllowedMethod(HttpMethod.PUT);// 添加允许PUT方法，默认设置里并没有。
        configuration.addAllowedMethod(HttpMethod.DELETE);// 添加允许DELETE方法，默认设置里并没有。
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 可以添加多个路径以支持跨域请求
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}